fix: prevent stale SDK session and tool-marker parroting after idle timeout#184
Merged
op7418 merged 1 commit intoop7418:mainfrom Mar 7, 2026
Conversation
…imeout After a stream idle timeout (330s), the SDK session ID remained in the database. The next user message attempted to resume this broken session, which failed and fell back to buildPromptWithHistory(). That function converted tool-use blocks into [Used tool: ...] and [Tool result: ...] plain-text markers, which Claude then parroted back as literal text instead of executing real tools. Three changes fix this end-to-end: 1. session PATCH endpoint (route.ts): accept sdk_session_id in the request body so it can be cleared via API. 2. idle timeout handler (stream-session-manager.ts): after emitting the timeout error, fire a PATCH request to clear the stale sdk_session_id from the database. This ensures the next message starts a fresh SDK session instead of attempting a doomed resume. 3. history fallback (claude-client.ts): strip tool_use and tool_result blocks from buildPromptWithHistory() output entirely. Previously these were rendered as [Used tool: X] / [Tool result: ...] text that Claude treated as its own output. Now only the text portions of assistant turns are included, with an explicit instruction that the history is a summary of already-executed turns. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
After a stream idle timeout (~330s), Claude's responses would contain raw tool markers like
[Used tool: Read]and[Tool result: ...]as plain text instead of actually executing tools. This made the conversation unusable — the user had to start a new session to recover.Root cause
Three problems combined to produce this bug:
Stale session ID persists after timeout: When the idle timeout fires, the client-side
AbortControllerkills the stream, but thesdk_session_idstays in the SQLite database. The next user message reads this stale ID and tries to resume the dead remote session.Resume fails silently into a bad fallback: The resume attempt fails (the remote session is gone), so
claude-client.tscatches the error and falls back tobuildPromptWithHistory(), which reconstructs conversation context from the local message database.History fallback produces parrot-able text:
buildPromptWithHistory()converted tool-use and tool-result content blocks into plain-text markers like[Used tool: Read]and[Tool result: file contents here...]. Claude interpreted these as text it had previously output and reproduced them verbatim in new responses.Fix (3 changes)
src/app/api/chat/sessions/[id]/route.ts— Addsdk_session_idsupport to the existing PATCH endpoint so it can be cleared via API call.src/lib/stream-session-manager.ts— In the idle timeout handler, after emitting the error event, fire a PATCH request to clear the stalesdk_session_idfrom the database. This ensures the next message starts a fresh SDK session instead of attempting a doomed resume.src/lib/claude-client.ts— RewritebuildPromptWithHistory()to striptool_useandtool_resultblocks entirely instead of converting them to[Used tool: ...]/[Tool result: ...]text. Now only the actual text content from assistant turns is preserved, with an explicit preamble telling Claude the history is a summary of already-executed turns that should not be repeated.How it works after the fix
Files changed
src/app/api/chat/sessions/[id]/route.tssdk_session_idin PATCH bodysrc/lib/stream-session-manager.tssdk_session_idon idle timeoutsrc/lib/claude-client.tsTest plan
STREAM_IDLE_TIMEOUT_MSto 10s)[Used tool: ...]text artifactsnpm run testpasses (typecheck + unit tests)🤖 Generated with Claude Code